// ==LICENSE-BEGIN==
// Copyright 2017 European Digital Reading Lab. All rights reserved.
// Licensed to the Readium Foundation under one or more contributor license agreements.
// Use of this source code is governed by a BSD-style license
// that can be found in the LICENSE file exposed on Github (readium) in the project repository.
// ==LICENSE-END=

import { createVerify } from "crypto";
import * as debug_ from "debug";
import { streamToBufferPromise } from "@r2-utils-js/_utils/stream/BufferUtils";
import { zipLoadPromise } from "@r2-utils-js/_utils/zip/zipFactory";
import { ICustomizationManifest } from "readium-desktop/common/readium/customization/manifest";
import { tryCatch } from "readium-desktop/utils/tryCatch";
import { extractCrc32OnZip } from "../tools/crc";
import * as path from "path";
import * as fs from "fs";
import { ICustomizationProfileProvisioned, ICustomizationProfileError, ICustomizationProfileProvisionedWithError } from "readium-desktop/common/redux/states/customization";
import { app } from "electron";
import { _CUSTOMIZATION_PROFILE_PUB_KEY } from "readium-desktop/preprocessor-directives";
import { URL_PROTOCOL_THORIUMHTTPS, URL_HOST_COMMON, URL_PATH_PREFIX_CUSTOMPROFILEZIP } from "readium-desktop/common/streamerProtocol";
import { encodeURIComponent_RFC3986 } from "@r2-utils-js/_utils/http/UrlUtils";
import Ajv from "ajv";
import addFormats from "ajv-formats";
import { diMainGet } from "../di";
import { TaJsonDeserialize } from "@r2-lcp-js/serializable";
import { OPDSPublication } from "@r2-opds-js/opds/opds2/opds2-publication";
import isURL from "validator/lib/isURL";
import { EXT_THORIUM } from "readium-desktop/common/extension";
import { customizationManifestJsonSchemaMinimal } from "readium-desktop/common/readium/customization/profile.schema";

// Logger
const debug = debug_("readium-desktop:main#utils/customization/provisioning");

export const customizationWellKnownFolder = path.join(app.getPath("userData"), "custom-profiles");

try {
    if (!fs.existsSync(customizationWellKnownFolder)) {
        fs.mkdirSync(customizationWellKnownFolder);
        debug(`Customization well-known folder created \"${customizationWellKnownFolder}\"`);
    }
} catch (e) {
    debug("ERROR!!: Customization well-known folder not created", e);
}

export let __CUSTOMIZATION_PROFILE_MANIFEST_RUNTIME_VALIDATION_AJV_ERRORS = "";
export function isCustomizationProfileManifest(data: any): data is ICustomizationManifest {

    const ajv = new Ajv();
    addFormats(ajv);

    const valid = ajv.validate(customizationManifestJsonSchemaMinimal, data);

    __CUSTOMIZATION_PROFILE_MANIFEST_RUNTIME_VALIDATION_AJV_ERRORS = ajv.errors?.length ? JSON.stringify(ajv.errors, null, 2) : "";

    return valid;
}


async function getManifestFromPackageFileName(packageFileName: string): Promise<ICustomizationManifest> {
    const packageAbsolutePath = path.join(customizationWellKnownFolder, packageFileName);
    const zip = await tryCatch(() => zipLoadPromise(packageAbsolutePath), "");
    if (!zip) {
        return Promise.reject("Not a ZIP package");
    }

    if (!zip.hasEntries()) {
        // return Promise.reject("LPF zip empty");
        return Promise.reject("Zip has no entries");
    }

    if (!zip.hasEntry("manifest.json")) {
        return Promise.reject("manifest not found");
    }

    const manifestStream = await zip.entryStreamPromise("manifest.json");
    const manifestBuffer = await streamToBufferPromise(manifestStream.stream);
    const manifest: ICustomizationManifest = JSON.parse(manifestBuffer.toString());

    if (manifest.version !== 1) {
        return Promise.reject("Not a valid manifestVersion");
    }

    if (!isCustomizationProfileManifest(manifest)) { // version 1

        debug("Error: ", __CUSTOMIZATION_PROFILE_MANIFEST_RUNTIME_VALIDATION_AJV_ERRORS);
        return Promise.reject("Manifest parsing error: " + __CUSTOMIZATION_PROFILE_MANIFEST_RUNTIME_VALIDATION_AJV_ERRORS);
    }

    return manifest;
}

async function checkIfProfilePackageSigned(manifest: ICustomizationManifest, packageFileName: string) {

    if (__TH__IS_DEV__ && manifest.signature === undefined) {
        return true;
    }

    if (!manifest.signature || !manifest.signature.value) {
        return Promise.reject("no signature found");
    }

    if (manifest.signature.key !== _CUSTOMIZATION_PROFILE_PUB_KEY) {
        return Promise.reject("manifest public key different from shipped public key");

    }

    const signatureValue = manifest.signature.value;
    const signatureKey = manifest.signature.key;
    const stringifiedManifest = JSON.stringify({...manifest, signature: undefined});

    const verify = createVerify("SHA256");
    verify.write(stringifiedManifest);
    verify.end();

    const verified = verify.verify(signatureKey, signatureValue, "hex");
    if (!verified) {
        return Promise.reject("manifest not verified from signature");
    }

    const packageAbsolutePath = path.join(customizationWellKnownFolder, packageFileName);
    const contentHash = await extractCrc32OnZip(packageAbsolutePath, "profile");
    if (manifest.contentHash !== contentHash) {
        return Promise.reject("manifest contentHash missmatch");
    }

    return true;
}

export function customizationPackageProvisioningCheckVersion(profilesProvisionedAndLatest: ICustomizationProfileProvisioned[], packagesNotProvisionedOrOnError: ICustomizationProfileProvisionedWithError[], profile: ICustomizationProfileProvisioned): [ICustomizationProfileProvisioned[], ICustomizationProfileProvisionedWithError[]] {

    const profileProvisionedWithSameId = profilesProvisionedAndLatest.filter(({id}) => id === profile.id);
    profileProvisionedWithSameId.push(profile);

    profileProvisionedWithSameId.sort(({version: va}, {version: vb}) => va - vb);

    const profileLastVersion = profileProvisionedWithSameId.pop();

    const provisionedProfile = profilesProvisionedAndLatest.filter(({id}) => !(id === profile.id));
    provisionedProfile.push(profileLastVersion);
    provisionedProfile.sort(({ id: a }, { id: b }) => a.localeCompare(b));

    packagesNotProvisionedOrOnError.push(...profileProvisionedWithSameId as ICustomizationProfileProvisionedWithError[]);
    return [provisionedProfile, packagesNotProvisionedOrOnError]; // [packagesProvisioned, packagesNotProvisionedOrOnError]
}

export async function customizationPackageProvisioningFromFolder(wellKnownFolder: string): Promise<[ICustomizationProfileProvisioned[], ICustomizationProfileProvisionedWithError[]]>{

    const results = fs.readdirSync(wellKnownFolder, {withFileTypes: true});
    
    let packagesNotProvisionedOrOnError: ICustomizationProfileProvisionedWithError[] = [];
    let packagesProvisionedAndLatest: ICustomizationProfileProvisioned[] = [];
    for (const dirent of results) {
        if (dirent.isFile() && path.extname(dirent.name) === EXT_THORIUM) {
            const packageFileName = dirent.name;
            debug("Found => ", packageFileName);
            const profileProvisionedOrOnError = await customizationPackageProvisioning(packageFileName);
            if ((profileProvisionedOrOnError as ICustomizationProfileError).error) {
                debug("ERROR: Profile not provisioned, due to error :", (profileProvisionedOrOnError as ICustomizationProfileError).message);
                packagesNotProvisionedOrOnError.push((profileProvisionedOrOnError as ICustomizationProfileError));
            } else {
                
                [packagesProvisionedAndLatest, packagesNotProvisionedOrOnError] = customizationPackageProvisioningCheckVersion(
                    packagesProvisionedAndLatest,
                    packagesNotProvisionedOrOnError,
                    profileProvisionedOrOnError as ICustomizationProfileProvisioned,
                );
            }
        }
    }

    return [packagesProvisionedAndLatest, packagesNotProvisionedOrOnError];
}

export async function customizationPackageProvisioning(packageFileName: string): Promise<ICustomizationProfileProvisionedWithError> {

    let manifest: ICustomizationManifest;
    let error = "";
    try {
        manifest = await customizationPackageProvisioningManifest(packageFileName);
    } catch (e) {
        debug("Error when provisioning this profile =>", packageFileName);
        error = `${e}`;
    }

    if (!manifest) {
        return { fileName: packageFileName, error: true, message: error } as ICustomizationProfileError;
    }

    const logoObj = manifest.images?.find((ln) => ln?.rel === "logo");
    debug("find manifest for this profile", manifest.identifier, manifest.version, " LOGO Obj:", logoObj);
    const baseUrl = `${URL_PROTOCOL_THORIUMHTTPS}://${URL_HOST_COMMON}/${URL_PATH_PREFIX_CUSTOMPROFILEZIP}/${encodeURIComponent_RFC3986(Buffer.from(manifest.identifier).toString("base64"))}/`;
    const logoUrl = baseUrl + encodeURIComponent_RFC3986(Buffer.from(logoObj.href).toString("base64"));

    const selfLinkUrl = manifest.links?.find(({ rel }) => rel === "self")?.href;

    const publicationsView = [];
    const publications = manifest.publications;
    if (publications?.length) {
        const opdsFeedViewConverter = diMainGet("opds-feed-view-converter");

        for (const opdsPubJson of publications) {

            const opdsPubJsonLinks = (opdsPubJson as any).links;
            if (typeof opdsPubJsonLinks === "object" && Array.isArray(opdsPubJsonLinks)) {
                for (const _link of opdsPubJsonLinks) {
                    debug("_link.href === \"", _link.href, "\"");
                    if (typeof _link.href === "string") {
                        if (isURL(_link.href)) {
                            // let's go !
                        } else {
                            _link.href = baseUrl + encodeURIComponent_RFC3986(Buffer.from(_link.href).toString("base64"));
                        }
                        debug("_link.href === \"", _link.href, "\"");
                    }
                }
            }

            const opdsPubJsonImages = (opdsPubJson as any).images;
            if (typeof opdsPubJsonImages === "object" && Array.isArray(opdsPubJsonImages)) {
                for (const _image of opdsPubJsonImages) {
                    debug("_image.href === \"", _image.href, "\"");
                    if (typeof _image.href === "string") {
                        if (isURL(_image.href)) {
                            // let's go !
                        } else {
                            _image.href = baseUrl + encodeURIComponent_RFC3986(Buffer.from(_image.href).toString("base64"));
                        }
                        debug("_image.href === \"", _image.href, "\"");
                    }
                }
            }

            debug("opdsPubJson:");
            debug(opdsPubJson);

            try {
                const opdsPublication = TaJsonDeserialize(
                    opdsPubJson,
                    OPDSPublication,
                );
                const opdsPubView = opdsFeedViewConverter.convertOpdsPublicationToView(opdsPublication, "/");
                if (opdsPubView) {
                    publicationsView.push(opdsPubView);
                }
            } catch (e) {
                debug("ERROR to load a publication from the profile", (opdsPubJson as any)?.metadata?.identifier);
                debug(e);
            }
        }

    }

    return {
        id: manifest.identifier,
        fileName: packageFileName,
        version: (new Date(manifest.modified || manifest.created)).getTime(),
        logoUrl,
        title: manifest.title,
        description: manifest.description,
        opdsPublicationView: publicationsView,
        selfLinkUrl,
    };
}

export async function customizationPackageProvisioningManifest(packageFileName: string): Promise<ICustomizationManifest> {

    debug("start provisioning => ", packageFileName);

    const manifest = await getManifestFromPackageFileName(packageFileName);
    await checkIfProfilePackageSigned(manifest, packageFileName);

    // TODO check ressources !?


    debug("Read manifest.json from ", packageFileName);
    debug(JSON.stringify(manifest, null, 4));
    return manifest;
}
